package org.redcross.openmapkit; import android.app.ProgressDialog; import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.util.Log; import com.spatialdev.osm.OSMMap; import com.spatialdev.osm.model.JTSModel; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.io.CountingInputStream; import com.spatialdev.osm.model.OSMDataSet; import org.redcross.openmapkit.odkcollect.ODKCollectHandler; /** * Created by Nicholas Hallahan on 1/28/15. * nhallahan@spatialdev.com* */ public class OSMMapBuilder extends AsyncTask<File, Long, JTSModel> { public static final float MIN_VECTOR_RENDER_ZOOM = 18; private static final String PERSISTED_OSM_FILES = "org.redcross.openmapkit.PERSISTED_OSM_FILES"; private static MapActivity mapActivity; private static SharedPreferences sharedPreferences; private static Set<String> persistedOSMFiles = new HashSet<>(); private static Set<String> loadedOSMFiles = new HashSet<>(); private static JTSModel jtsModel = new JTSModel(); private static ProgressDialog progressDialog; private static int totalFiles = 0; private static int completedFiles = 0; private static Set<OSMMapBuilder> activeBuilders = new HashSet<>(); private static long totalBytesLoaded = 0; private static long totalFileSizes = 0; private String fileName; private CountingInputStream countingInputStream; private long fileSize = 0; private long fileBytesLoaded = 0; // Should be set to true if we are loading edited OSM XML private boolean isOSMEdit = false; public static void buildMapFromExternalStorage(MapActivity ma) { mapActivity = ma; sharedPreferences = mapActivity.getPreferences(Context.MODE_PRIVATE); // sets persistedOSMFiles object we are about to use setPersistedOSMFilesFromSharedPreferences(); // load the previously selected OSM files in OpenMapKit for (String absPath : persistedOSMFiles) { if (!loadedOSMFiles.contains(absPath)) { ++totalFiles; File xmlFile = new File(absPath); OSMMapBuilder builder = new OSMMapBuilder(false); builder.executeOnExecutor(LARGE_STACK_THREAD_POOL_EXECUTOR, xmlFile); } } // load the edited OSM files in ODK Collect if (ODKCollectHandler.isODKCollectMode()) { List<File> editedOsmFiles = ODKCollectHandler.getODKCollectData().getEditedOSM(); for (File f : editedOsmFiles) { if (!loadedOSMFiles.contains(f.getAbsolutePath())) { ++totalFiles; OSMMapBuilder builder = new OSMMapBuilder(true); builder.executeOnExecutor(LARGE_STACK_THREAD_POOL_EXECUTOR, f); } } } if (totalFiles > 0) { setupProgressDialog(mapActivity); } else { OSMMap osmMap = new OSMMap(mapActivity.getMapView(), jtsModel, mapActivity, MIN_VECTOR_RENDER_ZOOM); mapActivity.setOSMMap(osmMap); } } /** * Returns a boolean array of what files have been loaded * * * @param files * @return */ public static boolean[] isFileArrayLoaded(File[] files) { int len = files.length; boolean[] isLoaded = new boolean[len]; for (int i=0; i < len; ++i) { String absPath = files[i].getAbsolutePath(); isLoaded[i] = loadedOSMFiles.contains(absPath); } return isLoaded; } /** * Returns a boolean array of what files have been previously * selected and persisted to be on the map. * * * * * @param files * @return */ public static boolean[] isFileArraySelected(File[] files) { int len = files.length; boolean[] isLoaded = new boolean[len]; for (int i=0; i < len; ++i) { String absPath = files[i].getAbsolutePath(); isLoaded[i] = persistedOSMFiles.contains(absPath); } return isLoaded; } public static void removeOSMFilesFromModel(Set<File> files) { if (files.size() < 1) { return; } for (File f : files) { String absPath = f.getAbsolutePath(); if (loadedOSMFiles.contains(absPath)) { jtsModel.removeDataSet(absPath); loadedOSMFiles.remove(absPath); persistedOSMFiles.remove(absPath); } } mapActivity.getMapView().invalidate(); updateSharedPreferences(); } public static void addOSMFilesToModel(Set<File> files) { if (files.size() < 1) { return; } for (File f : files) { String absPath = f.getAbsolutePath(); // Don't add something that is either in progress // or already on the map. if (persistedOSMFiles.contains(absPath)) { continue; } ++totalFiles; persistedOSMFiles.add(absPath); File xmlFile = new File(absPath); OSMMapBuilder builder = new OSMMapBuilder(false); builder.executeOnExecutor(LARGE_STACK_THREAD_POOL_EXECUTOR, xmlFile); } setupProgressDialog(mapActivity); mapActivity.getMapView().invalidate(); updateSharedPreferences(); } /** * This is used by the deployments, because the actual files get added * to the model in buildMapFromExternalStorage when the MapActivity * get's re-initiated. * * @param files */ private static void addOSMFilesToPersistedOSMFiles(Set<File> files) { for (File f : files) { persistedOSMFiles.add(f.getAbsolutePath()); } updateSharedPreferences(); } /** * The provided set of files gets added to the model, and all files * currently in the model that are not in the set are removed. * * @param files - the only files we want on the map */ public static void prepareMapToShowOnlyTheseOSM(Set<File> files) { Set<String> filePaths = new HashSet<>(); for (File f : files) { filePaths.add(f.getAbsolutePath()); } Set<File> filesToRemove = new HashSet<>(); for (String lf : loadedOSMFiles) { if (!filePaths.contains(lf)) { filesToRemove.add(new File(lf)); } } removeOSMFilesFromModel(filesToRemove); // We don't want to do this, because files in the persistedOSMFiles set // will get loaded by buildMapFromExternalStorage when the MapActivity // gets reloaded. //addOSMFilesToModel(files); // just adds it to the set, the set gets read later addOSMFilesToPersistedOSMFiles(files); } private static void updateSharedPreferences() { if (sharedPreferences == null) return; SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putStringSet(PERSISTED_OSM_FILES, persistedOSMFiles); editor.apply(); } private OSMMapBuilder(boolean isOSMEdit) { super(); this.isOSMEdit = isOSMEdit; activeBuilders.add(this); } protected static void setupProgressDialog(MapActivity mapActivity) { progressDialog = new ProgressDialog(mapActivity); progressDialog.setTitle("Loading OSM Data"); progressDialog.setMessage(""); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // progressDialog.setCancelable(false); progressDialog.setProgress(0); progressDialog.setMax(100); progressDialog.show(); } @Override protected JTSModel doInBackground(File... params) { File f = params[0]; fileName = f.getName(); String absPath = f.getAbsolutePath(); Log.i("BEGIN_PARSING", fileName); setFileSize(f.length()); try { InputStream is = new FileInputStream(f); countingInputStream = new CountingInputStream(is); OSMDataSet ds = OSMXmlParserInOSMMapBuilder.parseFromInputStream(countingInputStream, this); if (isOSMEdit) { jtsModel.mergeEditedOSMDataSet(absPath, ds); } else { jtsModel.addOSMDataSet(absPath, ds); } loadedOSMFiles.add(absPath); } catch (Exception e) { e.printStackTrace(); } return jtsModel; } @Override protected void onProgressUpdate(Long... progress) { long percent = progress[0]; long elementsRead = progress[1]; long nodesRead = progress[2]; long waysRead = progress[3]; long relationsRead = progress[4]; Log.i("PARSER_PROGRESS", "fileName=" + fileName + ", " + "percent=" + percent + ", " + "elementsRead=" + elementsRead + ", " + "nodesRead=" + nodesRead + ", " + "waysRead=" + waysRead + ", " + "relationsRead=" + relationsRead); progressDialog.setMessage("Parsing " + (completedFiles + 1) + " of " + totalFiles + " OSM XML Files."); progressDialog.setProgress((int)percent); } @Override protected void onPostExecute(JTSModel model) { ++completedFiles; // do this when everything is done loading if (completedFiles == totalFiles) { finishAndResetStaticState(); OSMMap osmMap = new OSMMap(mapActivity.getMapView(), model, mapActivity, MIN_VECTOR_RENDER_ZOOM); mapActivity.setOSMMap(osmMap); } } private void finishAndResetStaticState() { if(progressDialog.isShowing()) { progressDialog.dismiss(); } totalFiles = 0; completedFiles = 0; activeBuilders = new HashSet<>(); } public void updateFromParser(long elementReadCount, long nodeReadCount, long wayReadCount, long relationReadCount, long tagReadCount) { fileBytesLoaded = countingInputStream.getCount(); computeTotalProgress(); long percent = (long)(((float)totalBytesLoaded / (float)totalFileSizes) * 100); publishProgress(percent, elementReadCount, nodeReadCount, wayReadCount, relationReadCount, tagReadCount); } private void setFileSize(long size) { fileSize = size; } private long getFileSize() { return fileSize; } private long getFileBytesLoaded() { return fileBytesLoaded; } private static void computeTotalProgress() { totalBytesLoaded = 0; totalFileSizes = 0; for (OSMMapBuilder builder : activeBuilders) { long bytesLoaded = builder.getFileBytesLoaded(); long fileSize = builder.getFileSize(); totalBytesLoaded += bytesLoaded; totalFileSizes += fileSize; } } /** * We get the persisted OSM XML files, but we also check that they indeed are still * on the file system. * * @return */ private static void setPersistedOSMFilesFromSharedPreferences() { Set<String> sharedPrefSet = sharedPreferences.getStringSet(PERSISTED_OSM_FILES, loadedOSMFiles); persistedOSMFiles = new HashSet<>(); for (String path : sharedPrefSet) { if ((new File(path).exists())) { persistedOSMFiles.add(path); } } updateSharedPreferences(); } /** * CUSTOM THREAD POOL THAT HAS A LARGER STACK SIZE TO HANDLE LARGER OSM XML FILES * Sometimes the tags parsing recurses deeply... * http://stackoverflow.com/questions/27277861/increase-asynctask-stack-size */ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final int KEEP_ALIVE = 1; private static final ThreadFactory factory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { ThreadGroup group = new ThreadGroup("OSMMapBuilder_group"); return new Thread(group, r, "OSMMapBuilder_thread", 50000); } }; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>(); public static final Executor LARGE_STACK_THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, factory); }